﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using u = FinPlusUtility.Utility;

namespace Messaging2
{
    public class Transport:IDisposable
    {

        public Listener Listener;
        private Connection _connListen;
        private Connection _connSend;

        private Byte[] _buffer = null;
        private Dictionary<string, Message> _cacheMessages; //store for resend and prevent listen to own message
        private List<string> _cacheTopics;

        public Transport(string address, int port, IPAddress endpiont = null)
        {
            _cacheMessages = new Dictionary<string, Message>();
            _cacheTopics = new List<string>();
            _connSend = new Connection(address, port);
            _connListen = new Connection(address, port, IPAddress.Any);
            Listener = new Listener(_connListen, _cacheTopics);
            Listener.EventResend += ResendEvent;
        }

        public string Publish(Message message)
        {
            try
            {
                _buffer = Encoding.Unicode.GetBytes(message.ToString());
                _connSend.Client.Send(_buffer, _buffer.Length, _connSend.Endpoint);
                
                if (!message.RequestResendCache)//dont want to locally cache resend request
                {
                    _cacheMessages[message.Topic] = message;
                    if (!_cacheTopics.Contains(message.Topic)) 
                        _cacheTopics.Add(message.Topic);
                }

                return "Published";
            }
            catch (Exception e)
            {
                return e.ToString();
            }
        }

        public void Listen(string topic, bool requestCache = false)
        {
            Listener.Listen(topic); //single listener per transport;
            if (requestCache) Publish(new Message(topic +"|String|true|"));
        }
        
        public void Dispose()
        {
            if(Listener!=null) Listener.Dispose();
            if(_connSend !=null) _connSend.Dispose();
            if(_connListen != null) _connListen.Dispose();
        }

        private void ResendEvent(object sender, EventArgs e)
        {
            //if has send back out at moment all listeners will get
            MessageEventArgs res = (MessageEventArgs)e;
            if (_cacheMessages.ContainsKey(res.Message.Topic))
                Publish(_cacheMessages[res.Message.Topic]);
        }
    
    }

    public class Connection: IDisposable
    {

        public UdpClient Client = new UdpClient();
        public IPEndPoint Endpoint;
        public IPAddress Multicastaddress;

        public Connection(string address, int port)
        {
            Multicastaddress = IPAddress.Parse(address);
            Client.ExclusiveAddressUse = false;
            Endpoint = new IPEndPoint(Multicastaddress, port);
            Client.JoinMulticastGroup(Multicastaddress);
        }

        public Connection(string address, int port, IPAddress endPoint)
        {
            Multicastaddress = IPAddress.Parse(address);
            Client.ExclusiveAddressUse = false;
            Endpoint = new IPEndPoint(endPoint == null ? IPAddress.Any : endPoint, port);
            Client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            Client.ExclusiveAddressUse = false;
            Client.Client.Bind(Endpoint);
            Client.JoinMulticastGroup(Multicastaddress);
        }

        public void Dispose()
        {
            if(Client != null) Client.Close();
        }

    }

    public class Listener: IDisposable
    {
        public event EventHandler EventMessage;
        public event EventHandler EventResend;
        
        private List<string> _listenTopics = new List<string>();
        private List<string> _cacheTopics; //store for resend cache and prevent listen to own message
        private Connection _conn;
        private delegate void del(Connection conn);
        private del _ld;

        public Listener(Connection conn, List<string> cacheTopics)
        {
            _cacheTopics = cacheTopics;
            _conn = conn;

            _ld = new del(this.Listen);
            _ld.BeginInvoke(_conn, null, null);
  
        }

        public void Listen(string topic)
        {
            _listenTopics.Add(topic);//can have same topic more than once
        }
        
        public void Stop(string topic)
        {
            _listenTopics.Remove(topic);
        }

        private void Listen(Connection conn)
        {
            var handler = EventMessage;
            while (true)
            {
                try
                {
                    Byte[] data = _conn.Client.Receive(ref _conn.Endpoint);
                    Message message = new Message(Encoding.Unicode.GetString(data));

                    if (_cacheTopics.Contains(message.Topic) && message.RequestResendCache)
                    {
                        //if originated message and a request cache request publish again
                        var handlerResend = EventResend;
                        handlerResend(this, new MessageEventArgs(message));
                    }
                    else if (handler != null && _listenTopics.Contains(message.Topic)  && !message.RequestResendCache)
                    {
                        //only capture if has event handle listening to topic and not a request
                        handler(this, new MessageEventArgs(message));
                    }
                }
                catch (Exception e)
                {
                    if (_listenTopics.Contains("Error"))
                        handler(this, new MessageEventArgs(new Message(e)));
                }
            }
        }

        public void Dispose()
        {
            _ld = null;
            if(_conn !=null) _conn.Dispose();
        }

    }

    public class MessageEventArgs : EventArgs
    {
        public Message Message { get; set; }
        public MessageEventArgs(Message Message) { this.Message = Message; }
    }

    public enum ValType { String, Number, Array1D, Array2D, List, KVP };
    public enum ActType { New, Amend, Close, Unknown };

    public class Message
    {
        public string Topic { get; set; }
        public object Value { get; set; }
        public ValType ValueType { get; set; }
        public ActType ActionType { get { return actionType; } set { actionType = value; } }
        private ActType actionType = ActType.Unknown;
        
        public bool RequestResendCache = false; 

        public Exception Exception { get; set; }
        
        public Message(Exception exception)
        {
            Exception = exception;
            Topic = "Error";
        }

        public Message(string val, string topic, ActType actionType = ActType.Unknown)
        {
            Value = val;
            Topic = topic;
            ValueType = ValType.String;
            ActionType = actionType;
        }

        public Message(double val, string topic, ActType actionType = ActType.Unknown)
        {
            Value = val;
            Topic = topic;
            ValueType = ValType.String;
            ActionType = actionType;
        }

        public Message(object[] val, string topic, ActType actionType = ActType.Unknown)
        {
            Value = val;
            Topic = topic;
            ValueType = ValType.Array1D;
            ActionType = actionType;
        }

        public Message(object[,] val, string topic, ActType actionType = ActType.Unknown)
        {
            Value = val;
            Topic = topic;
            ValueType = ValType.Array2D;
            ActionType = actionType;
        }

        public Message(string val, char delimiterInfo = '|')
        {
            try
            {

                var s = val.Split(delimiterInfo);
                Topic = s[0];
                ValueType = (ValType)Enum.Parse(typeof(ValType), s[1]);
                RequestResendCache = bool.Parse(s[2]);
                ActionType = (ActType)Enum.Parse(typeof(ActType), s[3]);
    
                switch (ValueType)
                {
                    case ValType.String: Value = s[4]; break;
                    case ValType.Number: Value = double.Parse(s[4]); break;
                    case ValType.Array1D: Value = u.StringToArray1d(s[4]); break;
                    case ValType.Array2D: Value = u.StringToArray2d(s[4]); break;
                }
            }
            catch(Exception e)
            {
                Exception = e;
            }
        }

        public override string ToString()
        {
            return ToString('|');
        }

        public string ToString(char delimiterInfo = '|')
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                if (Value == null) return Exception.ToString();

                //basic info like attributes in tibo a bit simple
                sb.Append(Topic);
                sb.Append(delimiterInfo);
                sb.Append(ValueType.ToString());
                sb.Append(delimiterInfo);
                sb.Append(RequestResendCache.ToString());
                sb.Append(delimiterInfo);
                sb.Append(ActionType.ToString());
                sb.Append(delimiterInfo);

                switch (ValueType)
                {
                    case ValType.String: sb.Append(Value.ToString()); break;
                    case ValType.Number: sb.Append(Value.ToString()); break;
                    case ValType.Array1D: sb.Append(u.StringFromArray1d((object[])Value)); break;
                    case ValType.Array2D: sb.Append(u.StringFromArray2d((object[,])Value)); break;
                }
                return sb.ToString();
            }
            catch (Exception e)
            {
                return e.ToString();
            }
        }

    }
}
